﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using SSG.Tools;

namespace SSG.Model
{
    /// <summary>
    /// Airport with SIDs, STARs, runways
    /// </summary>
    public partial class Airport : Fix
    {
        private readonly List<Fix> _fixes = new List<Fix>();
        private readonly List<Sid> _sids = new List<Sid>();
        private readonly List<Star> _stars = new List<Star>();
        private readonly List<Runway> _runways = new List<Runway>();
        private readonly List<Approach> _approaches = new List<Approach>();
        public static readonly List<Airport> Airports = new List<Airport>();

        /// <summary>
        /// Based on AIRAC cycle
        /// </summary>
        public int AiracCycle { get; private set; }

        /// <summary>
        /// Based on AIRAC info
        /// </summary>
        public string AiracInfo { get; private set; }

        /// <summary>
        /// SIDs
        /// </summary>
        public List<Sid> Sids { get { return this._sids; } }

        /// <summary>
        /// STARs
        /// </summary>
        public List<Star> Stars { get { return this._stars; } }

        /// <summary>
        /// Runways
        /// </summary>
        public List<Runway> Runways { get { return this._runways; } }

        /// <summary>
        /// All fixes used with SID, STARs
        /// </summary>
        public List<Fix> Fixes { get { return this._fixes; } }

        /// <summary>
        /// Approaches
        /// </summary>
        public List<Approach> Approaches { get { return this._approaches; } }

        /// <summary>
        /// Transitions (resulting of approaches)
        /// </summary>
        public List<Transition> ApproachTransitions
        {
            get
            {
                List<Transition> transitions = new List<Transition>();
                foreach (Approach approach in this.Approaches) {
                    transitions.AddRange(approach.Transitions);
                }
                return transitions;
            }
        }

        /// <summary>
        /// Transitions (resulting of SIDs)
        /// </summary>
        public List<Transition> SidTransitions
        {
            get
            {
                List<Transition> transitions = new List<Transition>();
                foreach (Sid sid in this.Sids) {
                    transitions.AddRange(sid.Transitions);
                }
                return transitions;
            }
        }

        /// <summary>
        /// Transitions (resulting of SIDs)
        /// </summary>
        public List<Transition> StarTransitions
        {
            get
            {
                List<Transition> transitions = new List<Transition>();
                foreach (Star star in this.Stars) {
                    transitions.AddRange(star.Transitions);
                }
                return transitions;
            }
        }

        /// <summary>
        /// Transitions (resulting of SIDs)
        /// </summary>
        public List<Transition> Transitions
        {
            get
            {
                List<Transition> transitions = new List<Transition>();
                transitions.AddRange(this.SidTransitions);
                transitions.AddRange(this.StarTransitions);
                transitions.AddRange(this.ApproachTransitions);
                return transitions;
            }
        }

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="name"></param>
        /// <param name="description"></param>
        /// <param name="lat"></param>
        /// <param name="lng"></param>
        public Airport(string name, string description, double lat = 0, double lng = 0) : base(name, null, description, lat, lng)
        {
            this.AiracInfo = null;
            this.AiracCycle = -1;
        }

        /// <summary>
        /// Get the opposite runway
        /// </summary>
        /// <param name="runway"></param>
        /// <returns></returns>
        public Runway GetOppositeRunway(Runway runway)
        {
            if (runway == null) throw new ArgumentNullException("runway");
            return this.Runways.FirstOrDefault(rw => rw.IsOppositeRunway(runway));
        }

        /// <summary>
        /// Runway part of airport?
        /// </summary>
        /// <param name="runway"></param>
        /// <returns></returns>
        public bool HasRunway(Runway runway)
        {
            return runway != null && this.Runways.Any(rw => rw == runway);
        }

        /// <summary>
        /// Runway part of airport?
        /// </summary>
        /// <param name="runwayName"></param>
        /// <returns></returns>
        public bool HasRunway(string runwayName)
        {
            return !String.IsNullOrEmpty(runwayName) && this.Runways.Any(rw => runwayName.Equals(rw.Name, StringComparison.InvariantCultureIgnoreCase));
        }

        /// <summary>
        /// If given string contains runway designator, then return it.
        /// null otherwise
        /// </summary>
        /// <param name="nameCandidate"></param>
        /// <returns></returns>
        public string ContainingRunwayName(string nameCandidate)
        {
            return String.IsNullOrEmpty(nameCandidate) ? null : (from rw in this.Runways where nameCandidate.Contains(rw.Name) select rw.Name).FirstOrDefault();
        }

        /// <summary>
        /// Containing runway designator?
        /// </summary>
        /// <param name="nameCandidate"></param>
        /// <returns></returns>
        public bool ContainsRunwayName(string nameCandidate)
        {
            String cn = this.ContainingRunwayName(nameCandidate);
            bool c = !String.IsNullOrEmpty(cn);
            return c;
        }

        /// <summary>
        /// Get runways by name
        /// </summary>
        /// <param name="rwyNames"></param>
        /// <returns></returns>
        public List<Runway> GetRunways(List<String> rwyNames)
        {
            List<Runway> runways = new List<Runway>();
            if (rwyNames.NullSafeIsEmpty()) return runways;
            runways.AddRange(this.Runways.Where(rw => rwyNames.Contains(rw.Name)));
            return runways;
        }

        /// <summary>
        /// Add Procedure as SID, STAR, ..
        /// </summary>
        /// <param name="track"></param>
        /// <returns></returns>
        private bool AddTrack(Procedure track)
        {
            if (track == null) return false;
            bool added = false;
            // ReSharper disable CanBeReplacedWithTryCastAndCheckForNull
            if (track is Sid) {
                this._sids.Add((Sid) track);
                added = true;
            }
            else if (track is Star) {
                this._stars.Add((Star) track);
                added = true;
            }
            else if (track is Approach) {
                this._approaches.Add((Approach) track);
                added = true;
            }
            // ReSharper restore CanBeReplacedWithTryCastAndCheckForNull

            return added;
        }

        /// <summary>
        /// Add runways
        /// </summary>
        /// <param name="runway"></param>
        /// <returns></returns>
        public bool AddRunway(Runway runway)
        {
            if (runway == null) return false;
            if (BaseEntity.FindByName(runway.Name, this.Runways) != null) return false; // avoid duplicates
            this._runways.Add(runway);
            return true;
        }

        /// <summary>
        /// Get the tracks of the corresponding type.
        /// </summary>
        /// <param name="t"></param>
        /// <param name="settings"></param>
        /// <returns></returns>
        public List<Procedure> GetTracks(Type t, GeneratorSettings settings = null)
        {
            if (t == null) throw new ArgumentNullException("t");
            if (settings == null) settings = GeneratorSettings.DefaultSettings;
            if (settings == null) throw new InvalidOperationException("No default settings");

            if (t == typeof (Sid)) return this._sids.GeneralizeEnumeration<Procedure, Sid>();
            if (t == typeof (Star)) return this._stars.GeneralizeEnumeration<Procedure, Star>();
            if (t == typeof (Approach)) return this._approaches.GeneralizeEnumeration<Procedure, Approach>();
            if (t == typeof (Transition)) {
                if (settings.TransitionsIncludeAll) return this.Transitions.GeneralizeEnumeration<Procedure, Transition>();
                List<Transition> transitions = new List<Transition>();
                if (settings.TransitionsIncludeSids) transitions.AddRange(this.SidTransitions);
                if (settings.TransitionsIncludeStars) transitions.AddRange(this.StarTransitions);
                if (settings.TransitionsIncludeApproaches) transitions.AddRange(this.ApproachTransitions);
                return transitions.GeneralizeEnumeration<Procedure, Transition>();
            }
            throw new ArgumentException("Wrong type " + t.FullName);
        }

        /// <summary>
        /// Does the filter match?
        /// </summary>
        /// <param name="track"></param>
        /// <param name="filter"></param>
        /// <returns></returns>
        private bool MatchesFilter(BaseEntity track, NameFilter filter)
        {
            if (track == null) throw new ArgumentNullException("track");
            if (filter == null) throw new InvalidOperationException("Cannot get filter for " + track);
            if (filter.ModeIgnore) return false;
            if (filter.NamesWithNumber) return filter.ContainsNumber(track.Name);
            if (filter.NamesWithRunway) return this.ContainsRunwayName(track.Name);
            return false;
        }

        /// <summary>
        /// Meaningful representation.
        /// </summary>
        /// <returns></returns>
        public override string ToString()
        {
            StringBuilder sb = new StringBuilder();
            sb.AppendIfNotNullOrEmpty(this.Name);
            sb.AppendWithSeperator("Runways " + this._runways.Count);
            sb.AppendWithSeperator("Fixes " + this._fixes.Count);
            sb.AppendWithSeperator("SIDs " + this._sids.Count);
            sb.AppendWithSeperator("STARs " + this._stars.Count);
            sb.AppendWithSeperator("Approaches: " + this._approaches.Count);
            return sb.ToString();
        }

        /// <summary>
        /// Parse the content from the SID / STAR file.
        /// </summary>
        /// <param name="airportIcao"> </param>
        /// <param name="content"></param>
        /// <returns></returns>
        public static Airport AirportFromNavigraphPmdgSidStarFile(string airportIcao, string content)
        {
            if (string.IsNullOrEmpty(content)) throw new ArgumentNullException("content");
            if (string.IsNullOrEmpty(airportIcao)) throw new ArgumentNullException("airportIcao");
            string[] lines = content.Split('\n');
            Airport airport = BaseEntity.FindByName(airportIcao, Airport.Airports);
            if (airport == null) throw new InvalidOperationException("Missing airport " + airportIcao + " in global list");
            if (airport.Fixes.Count > 0) return airport; // already completely read

            // associate runways
            airport.LinkWithRunways(Runway.Runways);

            // ReSharper disable ForCanBeConvertedToForeach
            for (int line = 0; lines.Length > line; line++) {
                string cl = lines[line];
                if (String.IsNullOrWhiteSpace(cl)) continue;
                if (cl.StartsWith("//")) {
                    if (airport.AiracInfo == null && cl.Contains("AIRAC")) {
                        int cycle;
                        airport.AiracInfo = cl.Substring(cl.IndexOf(':') + 1).Trim();
                        if (int.TryParse(airport.AiracInfo.Substring(0, airport.AiracInfo.IndexOf(' ')), out cycle)) airport.AiracCycle = cycle;
                    }
                }
                else if (cl.StartsWith("FIX ")) {
                    Fix f = Fix.FixFromNavigraphPmdgFormat(cl, true);
                    if (f == null) throw new FmcParseException("FIX Line " + (line + 1) + " cannot be parsed: " + cl) { Content = cl };
                    airport.Fixes.Add(f);
                }
                else if (cl.StartsWith("RNW ")) {
                    // no longer required, globally linking with Runways
                    // from runways file
                }
                else if (cl.StartsWith("SID ") || cl.StartsWith("STAR ")) {
                    string[] subblock = Airport.IsolateBlock(lines, line + 1);
                    SidStar sidStar = Procedure.FromPmdgFormat(cl, airport) as SidStar;
                    if (sidStar == null) throw new FmcParseException("SID/STAR " + (line + 1) + " cannot be parsed: " + cl) {Content = cl};
                    if (!airport.AddTrack(sidStar)) throw new FmcParseException("Procedure " + sidStar + " cannot be added, derived from line " + (line + 1) + ": " + cl) {Content = cl};
                    sidStar.FromPmdgSidStarBlock(subblock);
                }
                else if (cl.StartsWith("APPROACH ")) {
                    Approach approach = Procedure.FromPmdgFormat(cl, airport) as Approach;
                    if (approach == null) throw new FmcParseException("Approach Line " + (line + 1) + " cannot be parsed: " + cl) {Content = cl};
                    if (!airport.AddTrack(approach)) throw new FmcParseException("Aproach " + approach + " cannot be added, derived from line " + (line + 1) + ": " + cl) {Content = cl};
                    Runway runway = approach.GuessRunway(airport.Runways);
                    string[] subblock = Airport.IsolateBlock(lines, line + 1);
                    foreach (string subblockLine in subblock) {
                        cl = subblockLine.Trim();
                        if (cl.StartsWith("TRANSITION")) {
                            Transition transition = Procedure.FromPmdgFormat(cl, airport, runway) as Transition;
                            if (transition == null) throw new FmcParseException("Transition Line " + (line + 1) + " cannot be parsed: " + subblockLine) {Content = cl};
                            approach.AddTransition(transition);
                        }
                        else {
                            throw new FmcParseException("Unknow subline " + cl + " for approach " + approach) {Content = cl};
                        }
                    }
                }
            }
            // ReSharper restore ForCanBeConvertedToForeach

            // return
            return airport;
        }

        /// <summary>
        /// Isolate sub block
        /// </summary>
        /// <param name="parts"></param>
        /// <param name="line"></param>
        /// <returns></returns>
        private static string[] IsolateBlock(string[] parts, int line)
        {
            if (parts.NullSafeIsEmpty()) return new string[0];
            List<string> p = new List<string>();
            for (int l = line; line < parts.Length; l++) {
                string cl = parts[l];
                if (cl.StartsWith(" "))
                    p.Add(cl);
                else
                    break;
            }
            return p.ToArray();
        }

        /// <summary>
        /// Object based on Navigraph PMDG format.
        /// </summary>
        /// <param name="airportLine"></param>
        /// <returns></returns>
        public static Airport AirportFromNavigraphPmdgFormat(string airportLine)
        {
            if (string.IsNullOrWhiteSpace(airportLine)) return null;
            airportLine = airportLine.Trim();

            // SAWB-64.244944 -56.630833
            // SCRM-62.190833 -58.986667
            //           1         2    
            // 0123456789012345678901234
            string icao = airportLine.Substring(0, 4).Trim();
            double lat, lng;
            if (!Double.TryParse(airportLine.Substring(4, 10).Trim(), NumberStyles.Float | NumberStyles.AllowThousands, Coordinates.ParseCulture, out lat)) return null;
            if (!Double.TryParse(airportLine.Substring(15, 10).Trim(), NumberStyles.Float | NumberStyles.AllowThousands, Coordinates.ParseCulture, out lng)) return null;
            Airport airport = new Airport(icao, null, lat, lng);
            return airport;
        }

        /// <summary>
        /// Read SID / STAR file for airport.
        /// </summary>
        /// <param name="file"></param>
        /// <returns></returns>
        public static Airport ReadNavigraphPmdgSidStarFile(string file)
        {
            if (String.IsNullOrEmpty(file)) return null;
            if (!File.Exists(file)) return null;
            String icao = Path.GetFileNameWithoutExtension(file);

            // create reader & read file
            string content = FileTools.ReadTextFile(file);
            return String.IsNullOrEmpty(content) ? null : Airport.AirportFromNavigraphPmdgSidStarFile(icao, content);
        }

        /// <summary>
        /// Read airports
        /// </summary>
        /// <param name="file"></param>
        /// <param name="linkWithRunways"></param>
        /// <returns></returns>
        public static List<Airport> ReadNavigraphPmdgAirports(string file, List<Runway> linkWithRunways = null)
        {
            if (String.IsNullOrEmpty(file)) return null;
            if (!File.Exists(file)) return null;

            // create reader & read file
            string content = FileTools.ReadTextFile(file);
            if (String.IsNullOrEmpty(content)) return null;
            string[] lines = content.Split('\n');
            List<Airport> airports = new List<Airport>();

            for (int line = 0; lines.Length > line; line++) {
                string cl = lines[line];
                if (String.IsNullOrWhiteSpace(cl)) continue;
                if (cl.StartsWith(";")) continue;
                // Airport stubs, later replace with complete tracks such as SIDs, STARs, ...
                Airport airport = Airport.AirportFromNavigraphPmdgFormat(cl);
                if (airport == null) throw new InvalidOperationException("Line " + (line + 1) + " cannot be parsed: " + cl);
                airports.Add(airport);
            }

            Airport.Airports.Clear();
            Airport.Airports.AddRange(airports);
            if (!linkWithRunways.NullSafeIsEmpty()) {
                Airport.LinkAirportsWithRunwaysBackground(linkWithRunways);
            }
            return Airport.Airports;
        }

        /// <summary>
        /// Asynchronoulsy link with runways
        /// </summary>
        /// <param name="runways"></param>
        /// <returns></returns>
        private static void LinkAirportsWithRunwaysBackground(List<Runway> runways)
        {
            Task<int> t = new Task<int>(() => Airport.LinkAirportsWithRunways(runways));
            t.Start();
        }

        /// <summary>
        /// Link with runways
        /// </summary>
        /// <param name="runways"></param>
        private static int LinkAirportsWithRunways(List<Runway> runways)
        {
            if (runways.NullSafeIsEmpty()) return 0;
            int linked = Airport.Airports.Sum(airport => airport.LinkWithRunways(runways));
            return linked;
        }

        /// <summary>
        /// Link airport with given runways
        /// </summary>
        /// <param name="runways"></param>
        public int LinkWithRunways(IEnumerable<Runway> runways)
        {
            if (!this.Runways.NullSafeIsEmpty()) return 0; // already linked
            int linked = 0;
            List<Runway> airportRunwaysUnlinked = runways.Where(rw => rw.Airport == null && rw.CanonicalName.StartsWith(this.Name, StringComparison.InvariantCultureIgnoreCase)).ToList();
            foreach (Runway runway in airportRunwaysUnlinked) {
                // link
                this.Runways.Add(runway);
                runway.Airport = this;
                if (String.IsNullOrEmpty(this.Description)) this.Description = runway.Description.Substring(0, runway.Description.Length - runway.Name.Length).Trim();
                linked++;
            }
            return linked;
        }
    }
}